{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# LRRM"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "This example demonstrates how to use `skrf`'s LRRM calibration. LRRM stands for Line-Reflect-Reflect-Match which are the calibration standards needed for the calibration. There are few different implementations of the LRRM that use slightly different assumptions and match model. `skrf`'s LRRM calibration uses the following assumptions:\n",
    "\n",
    " * Line standard needs to be known exactly.\n",
    " * First reflect's phase needs to be known within 90 degrees. It can be lossy and it's assumed to be identical on both ports.\n",
    " * Second reflect's |S11| is assumed to be known, it's phase also needs to be known within 90 degrees and it's assumed to be identical on both ports. The two reflects need to be different and their phase difference should be 180 degrees for the best accuracy.\n",
    " * Match is assumed to be a known resistance in series with unknown inductance. Match only needs to be measured on the first port.\n",
    " \n",
    "The calibration standards and measurements need to be given in the above order to the calibration routine. If the above assumptions are followed the calibration can solve the reflects, match and calibration parameters exactly."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## LRRM example with synthetic data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "import skrf\n",
    "from skrf.media import Coaxial\n",
    "\n",
    "skrf.stylely()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "###  Generate example data"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We first generate some synthetic error boxes and calibration standards. We will have two sets of the calibration standards. The real standards used for calibration that have parasitics and the approximate standards without parasitics that we will give to the calibration algorithm."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "freq = skrf.F(1, 100, 100, 'GHz')\n",
    "\n",
    "# 1.0 mm coaxial media for calibration error boxes\n",
    "coax = Coaxial(freq, z0_port=50, Dint=0.44e-3, Dout=1.0e-3, sigma=1e8)\n",
    "\n",
    "# Generate random error boxes\n",
    "X = coax.random(n_ports=2, name='X')\n",
    "Y = coax.random(n_ports=2, name='Y')\n",
    "\n",
    "# Random switch terms\n",
    "gamma_f = coax.random(n_ports=1, name='gamma_f')\n",
    "gamma_r = coax.random(n_ports=1, name='gamma_r')\n",
    "\n",
    "# Our guess of the standards. We assume they don't have any parasitics.\n",
    "oo_i = coax.open(nports=2, name='open')\n",
    "ss_i = coax.short(nports=2, name='short')\n",
    "# Match is only measured on one port. Resistance can be different from 50 ohms.\n",
    "m_i = coax.resistor(R=50, name='r') ** coax.short(nports=1)\n",
    "# Thru must be known exactly\n",
    "thru = coax.line(d=100, unit='um', name='thru')\n",
    "\n",
    "# Actual reflects with parasitics. They must be identical in both ports.\n",
    "# Short is slightly lossy.\n",
    "ss = coax.line(d=200, unit='um') ** coax.load(-0.98,nports=2, name='short') ** coax.line(d=200, unit='um')\n",
    "oo = coax.shunt_capacitor(10e-15) ** coax.open(nports=2, name='open') ** coax.shunt_capacitor(10e-15)\n",
    "\n",
    "# Match standard has inductance in series.\n",
    "match_l = 40e-12\n",
    "l = coax.inductor(L=match_l)\n",
    "m = l**m_i\n",
    "\n",
    "# Make two-port of the match with open on the second port.\n",
    "mm = skrf.two_port_reflect(m, coax.open(nports=1))\n",
    "# Make two-port for the match standard\n",
    "mm_i = coax.match(nports=2, name='load')\n",
    "\n",
    "# These are our guesses of the calibration standards.\n",
    "approx_ideals = [\n",
    "    thru,\n",
    "    ss_i,\n",
    "    oo_i,\n",
    "    mm_i\n",
    "    ]\n",
    "\n",
    "# These are the actual standards with parasitics.\n",
    "ideals = [\n",
    "    thru,\n",
    "    ss,\n",
    "    oo,\n",
    "    mm\n",
    "    ]\n",
    "\n",
    "# Make measurement of the standards using the random error boxes and switch terms.\n",
    "measured = [skrf.terminate(X**k**Y, gamma_f, gamma_r) for k in ideals]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Visualize the standards"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure()\n",
    "oo.plot_s_smith(m=0, n=0, label='Open')\n",
    "ss.plot_s_smith(m=0, n=0, label='Short')\n",
    "mm.plot_s_smith(m=0, n=0, label='Match')\n",
    "\n",
    "plt.figure()\n",
    "oo.plot_s_db(m=0, n=0, label='Open')\n",
    "ss.plot_s_db(m=0, n=0, label='Short')\n",
    "mm.plot_s_db(m=0, n=0, label='Match')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### LRRM calibration"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We pretend to not know the actual standards with parasitics and give only our approximations of the standards without parasitics and the measurements of the standard with parasitics."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "cal = skrf.LRRM(\n",
    "    ideals = approx_ideals,\n",
    "    measured = measured,\n",
    "    switch_terms = [gamma_f, gamma_r])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Visualizing the solved standards"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "LRRM calibration solves for the real standards. We can get the solved standards from the `cal` object. The solved standards should match the actual standards above."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure()\n",
    "cal.solved_r2.plot_s_smith(m=0, n=0, label='Open')\n",
    "cal.solved_r1.plot_s_smith(m=0, n=0, label='Short')\n",
    "cal.solved_m.plot_s_smith(m=0, n=0, label='Match')\n",
    "\n",
    "plt.figure()\n",
    "cal.solved_r2.plot_s_db(m=0, n=0, label='Open')\n",
    "cal.solved_r1.plot_s_db(m=0, n=0, label='Short')\n",
    "cal.solved_m.plot_s_db(m=0, n=0, label='Match')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {
    "tags": []
   },
   "source": [
    "The solved inductance of the match is also given as calibration output. It's given as an array but with the default options a single inductance is fitted over all frequencies."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "solved_match_l = cal.solved_l[0]\n",
    "print(f'Solved inductance {1e12*solved_match_l:.1f} pH, actual inductance {1e12*match_l:.1f} pH')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Calibrating DUT"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Measured DUT can be calibrated using the `apply_cal` method. The S-parameters should match exactly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Let's generate a DUT: 5 mm long 75 ohm line.\n",
    "dut = coax.line(d=5, unit='mm', z0=75)\n",
    "\n",
    "dut_measured = skrf.terminate(X**dut**Y, gamma_f, gamma_r)\n",
    "dut_cal = cal.apply_cal(dut_measured)\n",
    "\n",
    "plt.figure()\n",
    "dut.plot_s_db(m=0, n=0, label='Actual S11')\n",
    "dut.plot_s_db(m=1, n=0, label='Actual S21')\n",
    "dut_cal.plot_s_db(m=0, n=0, label='Calibrated S11')\n",
    "dut_cal.plot_s_db(m=1, n=0, label='Calibrated S21')\n",
    "plt.ylim([-20, 5])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Calibration verification using reflect |S11|\n",
    "\n",
    "During the calibration the second reflect |S11| is assumed to be known (|S11| = 1 in this case), but when a single inductance is fitted to the match standard this assumption can be broken. If the real match is not modeled well as known resistance in series with inductance it causes the reflect standard losslessness to be violated. By plotting the absolute value of the reflect we can get an idea on how good the calibration assumptions are.\n",
    "\n",
    "Let's first plot the open |S11| in the previous calibration. It should be exactly 0 dB if everything worked correctly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure()\n",
    "cal.solved_r2.plot_s_db(m=0, n=0, label='Solved open')\n",
    "plt.ylim([-0.01, 0.01])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Calibration with capacitive match\n",
    "\n",
    "The LRRM calibration model of the match is a resistance in series with an inductor. If the match also has parallel capacitance it won't be solved correctly and there will be errors in the corrected measurements.\n",
    "\n",
    "Let's define a new match standard and redo the calibration using it."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Match standard with series inductance and parallel capacitance.\n",
    "match_c = 20e-15\n",
    "match_l = 20e-12\n",
    "l = coax.inductor(L=match_l)\n",
    "c = coax.shunt_capacitor(match_c)\n",
    "m = c**l**m_i\n",
    "\n",
    "# Make two-port of the match with open on the second port.\n",
    "mm = skrf.two_port_reflect(m, coax.open(nports=1))\n",
    "\n",
    "# Redo the match measurement\n",
    "ideals[3] = mm\n",
    "measured[3] = skrf.terminate(X**mm**Y, gamma_f, gamma_r)\n",
    "\n",
    "# Redo the calibration\n",
    "cal = skrf.LRRM(\n",
    "    ideals = approx_ideals,\n",
    "    measured = measured,\n",
    "    switch_terms = [gamma_f, gamma_r]\n",
    "    )"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Visualize the capacitive match and the solved match"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Calibration tries to fit the inductance to the match as best as it can but it can't model the match exactly with an inductor. The closest fit is a negative valued inductor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure()\n",
    "mm.plot_s_smith(m=0, n=0, label='Actual')\n",
    "cal.solved_m.plot_s_smith(m=0, n=0, label='Solved')\n",
    "\n",
    "plt.figure()\n",
    "mm.plot_s_db(m=0, n=0, label='Actual')\n",
    "cal.solved_m.plot_s_db(m=0, n=0, label='Solved')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "solved_match_l = cal.solved_l[0]\n",
    "print(f'Solved inductance {1e12*solved_match_l:.1f} pH')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Plotting the |S11| of the open reveals that the solved open is not lossless indicating that some of the calibration assumptions were violated."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure()\n",
    "cal.solved_r2.plot_s_db(m=0, n=0, label='Solved open')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DUT measurement with incorrectly modeled match"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The incorrect match causes errors in the calibration parameters. The error increases at higher frequencies where the match modeling error is bigger."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dut_cal2 = cal.apply_cal(dut_measured)\n",
    "\n",
    "plt.figure()\n",
    "dut.plot_s_db(m=0, n=0, label='Actual S11')\n",
    "dut.plot_s_db(m=1, n=0, label='Actual S21')\n",
    "dut_cal2.plot_s_db(m=0, n=0, label='Calibrated S11')\n",
    "dut_cal2.plot_s_db(m=1, n=0, label='Calibrated S21')\n",
    "plt.ylim([-20, 5])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Match fit with inductance and capacitance\n",
    "\n",
    "LRRM has an option to use a match model with parallel capacitance which allows fitting the above match. The additional requirement for this fitting method is that the second reflect is open with some unknown capacitance. The open capacitance is fitted first assuming match is perfectly resistive weighting low frequencies where the assumption is likely to hold better. When the open capacitance is known match capacitance and inductance are fitted. The open and match fitting is iterated few times to refine the open and match guesses. This fitting method can be used by passing `match_fit = 'lc'` to the calibration method."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Redo the calibration using LC match model\n",
    "cal = skrf.LRRM(\n",
    "    ideals = approx_ideals,\n",
    "    measured = measured,\n",
    "    match_fit = 'lc',\n",
    "    switch_terms = [gamma_f, gamma_r]\n",
    "    )"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure()\n",
    "mm.plot_s_smith(m=0, n=0, label='Actual')\n",
    "cal.solved_m.plot_s_smith(m=0, n=0, label='Solved')\n",
    "\n",
    "plt.figure()\n",
    "mm.plot_s_db(m=0, n=0, label='Actual')\n",
    "cal.solved_m.plot_s_db(m=0, n=0, label='Solved')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "solved_match_l = cal.solved_l[0]\n",
    "solved_match_c = cal.solved_c[0]\n",
    "print(f'Solved inductance {1e12*solved_match_l:.1f} pH, actual inductance {1e12*match_l:.1f} pH')\n",
    "print(f'Solved capacitance {1e15*solved_match_c:.1f} fF, actual capacitance {1e15*match_c:.1f} fF')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Applying the calibration now to the measurements should give a close fit."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "dut_cal3 = cal.apply_cal(dut_measured)\n",
    "\n",
    "plt.figure()\n",
    "dut.plot_s_db(m=0, n=0, label='Actual S11')\n",
    "dut.plot_s_db(m=1, n=0, label='Actual S21')\n",
    "dut_cal3.plot_s_db(m=0, n=0, label='Calibrated S11')\n",
    "dut_cal3.plot_s_db(m=1, n=0, label='Calibrated S21')\n",
    "plt.ylim([-20, 5])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comparison with SOLT calibration"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The traditional two-port SOLT calibration assumes that all standards are known accurately. We can compare how it would perform with the same measurements with the same approximately known standards. Match needs to be also measured on the second port for SOLT, we assume it's identical to the first port. The randomly generated error boxes make the calibration especially difficult."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# SOLT requires match measurement on both ports.\n",
    "mm = skrf.two_port_reflect(m, m)\n",
    "measured[3] = skrf.terminate(X**mm**Y, gamma_f, gamma_r)\n",
    "\n",
    "# TwelveTerm assumes that thru is last.\n",
    "cal12 = skrf.TwelveTerm(\n",
    "    ideals = list(reversed(approx_ideals)),\n",
    "    measured = list(reversed(measured)),\n",
    "    n_thrus = 1,\n",
    "    )\n",
    "\n",
    "dut_cal12 = cal12.apply_cal(dut_measured)\n",
    "\n",
    "plt.figure()\n",
    "dut.plot_s_db(m=0, n=0, label='Actual S11')\n",
    "dut.plot_s_db(m=1, n=0, label='Actual S21')\n",
    "dut_cal12.plot_s_db(m=0, n=0, label='Calibrated S11')\n",
    "dut_cal12.plot_s_db(m=1, n=0, label='Calibrated S21')\n",
    "plt.ylim([-20, 5])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "anaconda-cloud": {},
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.9.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
